<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Page Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>


    <script>
        var Groover = (function() {
            // ensure webp is supported 
            function canEncode() {
                var canvas = document.createElement("canvas");
                canvas.width = 8;
                canvas.height = 8;
                return canvas.toDataURL("image/webp", 0.1).indexOf("image/webp") > -1;
            }
            if (!canEncode()) {
                return undefined;
            }
            var webmData = null;
            var clusterTimecode = 0;
            var clusterCounter = 0;
            var CLUSTER_MAX_DURATION = 30000;
            var frameNumber = 0;
            var width;
            var height;
            var frameDelay;
            var quality;
            var name;
            const videoMimeType = "video/webm"; // the only one.
            const frameMimeType = 'image/webp'; // can be no other
            const S = String.fromCharCode;
            const dataTypes = {
                object: function(data) {
                    return toBlob(data);
                },
                number: function(data) {
                    return stream.num(data);
                },
                string: function(data) {
                    return stream.str(data);
                },
                array: function(data) {
                    return data;
                },
                double2Str: function(num) {
                    var c = new Uint8Array((new Float64Array([num])).buffer);
                    return S(c[7]) + S(c[6]) + S(c[5]) + S(c[4]) + S(c[3]) + S(c[2]) + S(c[1]) + S(c[0]);
                }
            };

            const stream = {
                num: function(num) { // writes int
                    var parts = [];
                    while (num > 0) {
                        parts.push(num & 0xff);
                        num = num >> 8;
                    }
                    return new Uint8Array(parts.reverse());
                },
                str: function(str) { // writes string
                    var i, len, arr;
                    len = str.length;
                    arr = new Uint8Array(len);
                    for (i = 0; i < len; i++) {
                        arr[i] = str.charCodeAt(i);
                    }
                    return arr;
                },
                compInt: function(num) { // could not find full details so bit of a guess
                    if (num < 128) { // number is prefixed with a bit (1000 is on byte 0100 two, 0010 three and so on)
                        num += 0x80;
                        return new Uint8Array([num]);
                    } else
                    if (num < 0x4000) {
                        num += 0x4000;
                        return new Uint8Array([num >> 8, num])
                    } else
                    if (num < 0x200000) {
                        num += 0x200000;
                        return new Uint8Array([num >> 16, num >> 8, num])
                    } else
                    if (num < 0x10000000) {
                        num += 0x10000000;
                        return new Uint8Array([num >> 24, num >> 16, num >> 8, num])
                    }
                }
            }
            const ids = { // header names and values
                videoData: 0x1a45dfa3,
                Version: 0x4286,
                ReadVersion: 0x42f7,
                MaxIDLength: 0x42f2,
                MaxSizeLength: 0x42f3,
                DocType: 0x4282,
                DocTypeVersion: 0x4287,
                DocTypeReadVersion: 0x4285,
                Segment: 0x18538067,
                Info: 0x1549a966,
                TimecodeScale: 0x2ad7b1,
                MuxingApp: 0x4d80,
                WritingApp: 0x5741,
                Duration: 0x4489,
                Tracks: 0x1654ae6b,
                TrackEntry: 0xae,
                TrackNumber: 0xd7,
                TrackUID: 0x63c5,
                FlagLacing: 0x9c,
                Language: 0x22b59c,
                CodecID: 0x86,
                CodecName: 0x258688,
                TrackType: 0x83,
                Video: 0xe0,
                PixelWidth: 0xb0,
                PixelHeight: 0xba,
                Cluster: 0x1f43b675,
                Timecode: 0xe7,
                Frame: 0xa3,
                Keyframe: 0x9d012a,
                FrameBlock: 0x81,
            };
            const keyframeD64Header = '\x9d\x01\x2a'; //VP8 keyframe header 0x9d012a
            const videoDataPos = 1; // data pos of frame data header
            const defaultDelay = dataTypes.double2Str(1000 / 25);
            const header = [ // structure of webM header/chunks what ever they are called.
                ids.videoData, [
                    ids.Version, 1,
                    ids.ReadVersion, 1,
                    ids.MaxIDLength, 4,
                    ids.MaxSizeLength, 8,
                    ids.DocType, 'webm',
                    ids.DocTypeVersion, 2,
                    ids.DocTypeReadVersion, 2
                ],
                ids.Segment, [
                    ids.Info, [
                        ids.TimecodeScale, 1000000,
                        ids.MuxingApp, 'Groover',
                        ids.WritingApp, 'Groover',
                        ids.Duration, 0
                    ],
                    ids.Tracks, [
                        ids.TrackEntry, [
                            ids.TrackNumber, 1,
                            ids.TrackUID, 1,
                            ids.FlagLacing, 0, // always o
                            ids.Language, 'und', // undefined I think this means
                            ids.CodecID, 'V_VP8', // These I think must not change
                            ids.CodecName, 'VP8', // These I think must not change
                            ids.TrackType, 1,
                            ids.Video, [
                                ids.PixelWidth, 0,
                                ids.PixelHeight, 0
                            ]
                        ]
                    ]
                ]
            ];

            function getHeader() {
                header[3][2][3] = name;
                header[3][2][5] = name;
                header[3][2][7] = dataTypes.double2Str(frameDelay);
                header[3][3][1][15][1] = width;
                header[3][3][1][15][3] = height;

                function create(dat) {
                    var i, kv, data;
                    data = [];
                    for (i = 0; i < dat.length; i += 2) {
                        kv = {
                            i: dat[i]
                        };
                        if (Array.isArray(dat[i + 1])) {
                            kv.d = create(dat[i + 1]);
                        } else {
                            kv.d = dat[i + 1];
                        }
                        data.push(kv);
                    }
                    return data;
                }
                return create(header);
            }

            function addCluster() {
                webmData[videoDataPos].d.push({
                    i: ids.Cluster,
                    d: [{
                        i: ids.Timecode,
                        d: Math.round(clusterTimecode)
                    }]
                }); // Fixed bug with Round
                clusterCounter = 0;
            }

            function addFrame(frame) {
                var VP8, kfS, riff;
                riff = getWebPChunks(atob(frame.toDataURL(frameMimeType, quality).slice(23)));
                VP8 = riff.RIFF[0].WEBP[0];
                kfS = VP8.indexOf(keyframeD64Header) + 3;
                frame = {
                    width: ((VP8.charCodeAt(kfS + 1) << 8) | VP8.charCodeAt(kfS)) & 0x3FFF,
                    height: ((VP8.charCodeAt(kfS + 3) << 8) | VP8.charCodeAt(kfS + 2)) & 0x3FFF,
                    data: VP8,
                    riff: riff
                };
                if (clusterCounter > CLUSTER_MAX_DURATION) {
                    addCluster();
                }
                webmData[videoDataPos].d[webmData[videoDataPos].d.length - 1].d.push({
                    i: ids.Frame,
                    d: S(ids.FrameBlock) + S(Math.round(clusterCounter) >> 8) + S(Math.round(clusterCounter) & 0xff) + S(128) + frame.data.slice(4),
                });
                clusterCounter += frameDelay;
                clusterTimecode += frameDelay;
                webmData[videoDataPos].d[0].d[3].d = dataTypes.double2Str(clusterTimecode);
            }

            function startEncoding() {
                frameNumber = clusterCounter = clusterTimecode = 0;
                webmData = getHeader();
                addCluster();
            }

            function toBlob(vidData) {
                var data, i, vData, len;
                vData = [];
                for (i = 0; i < vidData.length; i++) {
                    data = dataTypes[typeof vidData[i].d](vidData[i].d);
                    len = data.size || data.byteLength || data.length;
                    vData.push(stream.num(vidData[i].i));
                    vData.push(stream.compInt(len));
                    vData.push(data)
                }
                return new Blob(vData, {
                    type: videoMimeType
                });
            }

            function getWebPChunks(str) {
                var offset, chunks, id, len, data;
                offset = 0;
                chunks = {};
                while (offset < str.length) {
                    id = str.substr(offset, 4);
                    // value will have top bit on (bit 32) so not simply a bitwise operation
                    // Warning little endian (Will not work on big endian systems)
                    len = new Uint32Array(
                        new Uint8Array([
                            str.charCodeAt(offset + 7),
                            str.charCodeAt(offset + 6),
                            str.charCodeAt(offset + 5),
                            str.charCodeAt(offset + 4)
                        ]).buffer)[0];
                    id = str.substr(offset, 4);
                    chunks[id] = chunks[id] === undefined ? [] : chunks[id];
                    if (id === 'RIFF' || id === 'LIST') {
                        chunks[id].push(getWebPChunks(str.substr(offset + 8, len)));
                        offset += 8 + len;
                    } else if (id === 'WEBP') {
                        chunks[id].push(str.substr(offset + 8));
                        break;
                    } else {
                        chunks[id].push(str.substr(offset + 4));
                        break;
                    }
                }
                return chunks;
            }

            function Encoder(fps, _quality = 0.8, _name = "Groover") {
                this.fps = fps;
                this.quality = quality = _quality;
                this.frameDelay = frameDelay = 1000 / fps;
                this.frame = 0;
                this.width = width = null;
                this.timecode = 0;
                this.name = name = _name;
            }
            Encoder.prototype = {
                addFrame: function(frame) {
                    if ('canvas' in frame) {
                        frame = frame.canvas;
                    }
                    if (width === null) {
                        this.width = width = frame.width,
                            this.height = height = frame.height
                        startEncoding();
                    } else
                    if (width !== frame.width || height !== frame.height) {
                        throw RangeError("Frame size error. Frames must be the same size.");
                    }
                    addFrame(frame);
                    this.frame += 1;
                    this.timecode = clusterTimecode;
                },
                toBlob: function() {
                    return toBlob(webmData);
                }
            }
            return {
                Video: Encoder,
            }
        })()
    </script>
</body>

</html>